﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Arch.Common.Proxy;

namespace Arch.Client.Proxy
{
    public class NotifyProxyTypeBuilder : IProxyTypeBuilder
    {
        protected Dictionary<Type, Type> Mappings = new Dictionary<Type, Type>();
        protected Dictionary<Type, Type> ReverseMappings = new Dictionary<Type, Type>();

        public AssemblyBuilder AssemblyBuilder { get; set; }
        public ModuleBuilder ModuleBuilder { get; set; }

        public NotifyProxyTypeBuilder()
            : this("DynamicProxyModel.dll")
        {
        }

        protected NotifyProxyTypeBuilder(string name)
        {
            var assemblyName = new AssemblyName { Name = name };
            AssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            ModuleBuilder = AssemblyBuilder.DefineDynamicModule(assemblyName.Name);
        }


        public Type GetProxy<T>()
        {
            return GetProxy(typeof(T));
        }

        public Type GetProxy(Type parent)
        {
            if (!Mappings.ContainsKey(parent))
            {
                if (!parent.IsSealed && parent.GetConstructor(new Type[0]) != null)
                {
                    Type proxyType = CreateProxyType(parent);
                    Mappings[parent] = proxyType;
                    ReverseMappings[proxyType] = parent;
                }
                else
                {
                    ReverseMappings[parent] = Mappings[parent] = parent;
                }
            }
            return Mappings[parent];
        }

        public Type GetUnproxy<T>()
        {
            return GetProxy(typeof(T));
        }

        public Type GetUnproxy(Type proxyType)
        {
            if (ReverseMappings.ContainsKey(proxyType)) return ReverseMappings[proxyType];
            return proxyType;
        }

        protected Type CreateProxyType(Type parent)
        {
            TypeBuilder typeBuilder = ModuleBuilder.DefineType(parent.Name + "NotifyProxy",
                TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass |
                TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout,
                parent, new Type[] { typeof(INotifyPropertyChanged) });
            typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanged));

            MethodBuilder raisePropertyChanged = CreateNotifyPropertyChanged(typeBuilder);
            CreateProperties(typeBuilder, parent, raisePropertyChanged);

            return typeBuilder.CreateType();
        }

        protected void CreateProperties(TypeBuilder typeBuilder, Type parent, MethodBuilder raiseEvent)
        {
            foreach (PropertyInfo pinfo in parent.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && p.CanWrite))
            {
                if (pinfo.GetSetMethod().IsVirtual)
                {
                    PropertyBuilder pb = typeBuilder.DefineProperty(
                        pinfo.Name, PropertyAttributes.None, pinfo.PropertyType, Type.EmptyTypes);
                    const MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName |
                                                        MethodAttributes.HideBySig | MethodAttributes.Virtual;
                    MethodBuilder getMethod =
                        typeBuilder.DefineMethod(
                            "get_" + pinfo.Name, getSetAttr, pinfo.PropertyType, Type.EmptyTypes);

                    ILGenerator gen = getMethod.GetILGenerator();
                    gen.Emit(OpCodes.Ldarg_0);
                    gen.Emit(OpCodes.Call, pinfo.GetGetMethod());
                    gen.Emit(OpCodes.Ret);
                    pb.SetGetMethod(getMethod);

                    MethodBuilder setMethod =
                        typeBuilder.DefineMethod("set_" + pinfo.Name, getSetAttr, null, new Type[] { pinfo.PropertyType });
                    gen = setMethod.GetILGenerator();
                    gen.Emit(OpCodes.Ldarg_0);
                    gen.Emit(OpCodes.Ldarg_1);
                    gen.Emit(OpCodes.Call, pinfo.GetSetMethod());
                    gen.Emit(OpCodes.Ldarg_0);
                    gen.Emit(OpCodes.Ldstr, pinfo.Name);
                    gen.Emit(OpCodes.Call, raiseEvent);
                    gen.Emit(OpCodes.Ret);
                    pb.SetSetMethod(setMethod);
                }
            }
        }

        protected MethodBuilder CreateNotifyPropertyChanged(TypeBuilder typeBuilder)
        {
            MethodInfo combine = typeof(Delegate).GetMethod("Combine", new Type[] { typeof(Delegate), typeof(Delegate) });
            MethodInfo remove = typeof(Delegate).GetMethod("Remove", new Type[] { typeof(Delegate), typeof(Delegate) });
            MethodInfo invoke = typeof(PropertyChangedEventHandler).GetMethod("Invoke");
            FieldBuilder eventPropertyChanged = typeBuilder.DefineField("PropertyChanged", typeof(PropertyChangedEventHandler), FieldAttributes.Private);
            ConstructorInfo createEventArgs = typeof(PropertyChangedEventArgs).GetConstructor(new Type[] { typeof(String) });


            MethodBuilder addPropertyChanged = typeBuilder.DefineMethod(
                "add_PropertyChanged", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName |
                    MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot,
                typeof(void), new Type[] { typeof(PropertyChangedEventHandler) });

            ILGenerator gen = addPropertyChanged.GetILGenerator();
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldfld, eventPropertyChanged);
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(OpCodes.Call, combine);
            gen.Emit(OpCodes.Castclass, typeof(PropertyChangedEventHandler));
            gen.Emit(OpCodes.Stfld, eventPropertyChanged);
            gen.Emit(OpCodes.Ret);

            MethodBuilder removePropertyChanged = typeBuilder.DefineMethod(
                "remove_PropertyChanged", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName |
                    MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot,
                typeof(void), new Type[] { typeof(PropertyChangedEventHandler) });

            gen = removePropertyChanged.GetILGenerator();
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldfld, eventPropertyChanged);
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(OpCodes.Call, remove);
            gen.Emit(OpCodes.Castclass, typeof(PropertyChangedEventHandler));
            gen.Emit(OpCodes.Stfld, eventPropertyChanged);
            gen.Emit(OpCodes.Ret);

            MethodBuilder raisePropertyChanged = typeBuilder.DefineMethod(
                "RaisePropertyChanged", MethodAttributes.Public,
                typeof(void), new Type[] { typeof(String) });
            gen = raisePropertyChanged.GetILGenerator();
            Label lblDelegateOk = gen.DefineLabel();

            gen.DeclareLocal(typeof(PropertyChangedEventHandler));
            gen.Emit(OpCodes.Nop);
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldfld, eventPropertyChanged);
            gen.Emit(OpCodes.Stloc_0);
            gen.Emit(OpCodes.Ldloc_0);
            gen.Emit(OpCodes.Ldnull);
            gen.Emit(OpCodes.Ceq);
            gen.Emit(OpCodes.Brtrue, lblDelegateOk);
            gen.Emit(OpCodes.Ldloc_0);
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(OpCodes.Newobj, createEventArgs);
            gen.Emit(OpCodes.Callvirt, invoke);
            gen.MarkLabel(lblDelegateOk);
            gen.Emit(OpCodes.Ret);

            EventBuilder pcevent = typeBuilder.DefineEvent("PropertyChanged", EventAttributes.None, typeof(PropertyChangedEventHandler));
            pcevent.SetRaiseMethod(raisePropertyChanged);
            pcevent.SetAddOnMethod(addPropertyChanged);
            pcevent.SetRemoveOnMethod(removePropertyChanged);

            return raisePropertyChanged;
        }
    }
}